W ramach projektu, zbudowano klasyfikator określający czy grzyb jest trujący na podstawie jego cech wyglądu zewnętrznego. Podjęto także próbę interpretacji klasyfikatora z wykorzystaniem pakietu DALEX oraz wyjaśnienia predykcji dla wybranej obserwacji.
Dane zostały udostępnione w kwietniu 2021r. przez UCI Machine Learning Repository (http://archive.ics.uci.edu/ml/datasets/Secondary+Mushroom+Dataset) i zawierają 61069 hipotetycznych obserwacji grzybów, które zostały zasymulowane losowo na podstawie popularnego zbioru Mushroom Data Set (1987 r.), zawierającego informacje na temat 173 gatunków grzybów. Zbiór danych został utworzony w ramach pracy dyplomowej D. Heidera i G. Hattaba na Uniwersytecie w Marburgu.
Zbiór składa się ze zmiennej celu class determinującej czy grzyb jest trujący oraz 20 zmiennych.
Atrybuty:
class: poisonous=p, edibile=e (binary) => zmienna celu
(n: nominal, m: metrical)
cap-diameter (m) - rozmiar kapelusza [cm] cap-shape (n) - kształt kapelusza
bell=b, conical=c, convex=x, flat=f, sunken=s, spherical=p, others=o
cap-surface (n) - powierzchnia kapelusza
fibrous=i, grooves=g, scaly=y, smooth=s, shiny=h, leathery=l, silky=k, sticky=t, wrinkled=w, fleshy=e
cap-color (n) - kolor kapelusza
brown=n, buff=b, gray=g, green=r, pink=p, purple=u, red=e, white=w, yellow=y, blue=l, orange=o, black=k
does-bruise-bleed (n) - czy sinieje lub "krawawi"
bruises-or-bleeding=t,no=f
gill-attachment (n) - rodzaj połączenia blaszek z trzonem
adnate=a, adnexed=x, decurrent=d, free=e, sinuate=s, pores=p, none=f, unknown=?
gill-spacing (n) - odległość między blaszkami
close=c, distant=d, none=f
gill-color (n) - kolor blaszek
see cap-color + none=f
stem-height (m) - wysokość trzonu [cm]
stem-width (m) - szerokość trzonu [mm]
stem-root (n) - kształ grzybni
bulbous=b, swollen=s, club=c, cup=u, equal=e, rhizomorphs=z, rooted=r
stem-surface (n) - powierzchnia trzonu
see cap-surface + none=f
stem-color (n) - kolor trzonu
see cap-color + none=f
veil-type (n) - rodzaj zasnówki
partial=p, universal=u
veil-color (n) - kolor zasnówki
see cap-color + none=f
has-ring (n) - czy posiada pierścień
ring=t, none=f
ring-type (n) - rodzaj pierścienia
cobwebby=c, evanescent=e, flaring=r, grooved=g, large=l, pendant=p, sheathing=s, zone=z, scaly=y, movable=m, none=f, unknown=?
spore-print-color (n) - kolor wysypu zarodników
see cap color
habitat (n) - występowanie
grasses=g, leaves=l, meadows=m, paths=p, heaths=h, urban=u, waste=w, woods=d
season (n) - pora roku występowania
spring=s, summer=u, autumn=a, winter=w
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import preprocessing
mushrooms = pd.read_csv("secondary_data.csv", sep=";")
mushrooms.shape
(61069, 21)
mushrooms.head()
| class | cap-diameter | cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-height | ... | stem-root | stem-surface | stem-color | veil-type | veil-color | has-ring | ring-type | spore-print-color | habitat | season | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | p | 15.26 | x | g | o | f | e | NaN | w | 16.95 | ... | s | y | w | u | w | t | g | NaN | d | w |
| 1 | p | 16.60 | x | g | o | f | e | NaN | w | 17.99 | ... | s | y | w | u | w | t | g | NaN | d | u |
| 2 | p | 14.07 | x | g | o | f | e | NaN | w | 17.80 | ... | s | y | w | u | w | t | g | NaN | d | w |
| 3 | p | 14.17 | f | h | e | f | e | NaN | w | 15.77 | ... | s | y | w | u | w | t | p | NaN | d | w |
| 4 | p | 14.64 | x | h | o | f | e | NaN | w | 16.53 | ... | s | y | w | u | w | t | p | NaN | d | w |
5 rows × 21 columns
mushrooms.tail()
| class | cap-diameter | cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-height | ... | stem-root | stem-surface | stem-color | veil-type | veil-color | has-ring | ring-type | spore-print-color | habitat | season | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 61064 | p | 1.18 | s | s | y | f | f | f | f | 3.93 | ... | NaN | NaN | y | NaN | NaN | f | f | NaN | d | a |
| 61065 | p | 1.27 | f | s | y | f | f | f | f | 3.18 | ... | NaN | NaN | y | NaN | NaN | f | f | NaN | d | a |
| 61066 | p | 1.27 | s | s | y | f | f | f | f | 3.86 | ... | NaN | NaN | y | NaN | NaN | f | f | NaN | d | u |
| 61067 | p | 1.24 | f | s | y | f | f | f | f | 3.56 | ... | NaN | NaN | y | NaN | NaN | f | f | NaN | d | u |
| 61068 | p | 1.17 | s | s | y | f | f | f | f | 3.25 | ... | NaN | NaN | y | NaN | NaN | f | f | NaN | d | u |
5 rows × 21 columns
mushrooms.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61069 entries, 0 to 61068 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 class 61069 non-null object 1 cap-diameter 61069 non-null float64 2 cap-shape 61069 non-null object 3 cap-surface 46949 non-null object 4 cap-color 61069 non-null object 5 does-bruise-or-bleed 61069 non-null object 6 gill-attachment 51185 non-null object 7 gill-spacing 36006 non-null object 8 gill-color 61069 non-null object 9 stem-height 61069 non-null float64 10 stem-width 61069 non-null float64 11 stem-root 9531 non-null object 12 stem-surface 22945 non-null object 13 stem-color 61069 non-null object 14 veil-type 3177 non-null object 15 veil-color 7413 non-null object 16 has-ring 61069 non-null object 17 ring-type 58598 non-null object 18 spore-print-color 6354 non-null object 19 habitat 61069 non-null object 20 season 61069 non-null object dtypes: float64(3), object(18) memory usage: 9.8+ MB
Na wycinku zbioru danych widać, że pojawia się trochę NA's oraz, że występują 2 typy zmiennych: float i object.
Zmienna celu class oraz pozostałe zmienne jakościowe zostaną zmienione na category.
cols = list(mushrooms.columns)
print(cols)
['class', 'cap-diameter', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color', 'stem-height', 'stem-width', 'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color', 'habitat', 'season']
col_cat = ['class', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color',
'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color','habitat',
'season']
for col in col_cat:
mushrooms[col] = mushrooms[col].astype('category')
mushrooms.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61069 entries, 0 to 61068 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 class 61069 non-null category 1 cap-diameter 61069 non-null float64 2 cap-shape 61069 non-null category 3 cap-surface 46949 non-null category 4 cap-color 61069 non-null category 5 does-bruise-or-bleed 61069 non-null category 6 gill-attachment 51185 non-null category 7 gill-spacing 36006 non-null category 8 gill-color 61069 non-null category 9 stem-height 61069 non-null float64 10 stem-width 61069 non-null float64 11 stem-root 9531 non-null category 12 stem-surface 22945 non-null category 13 stem-color 61069 non-null category 14 veil-type 3177 non-null category 15 veil-color 7413 non-null category 16 has-ring 61069 non-null category 17 ring-type 58598 non-null category 18 spore-print-color 6354 non-null category 19 habitat 61069 non-null category 20 season 61069 non-null category dtypes: category(18), float64(3) memory usage: 2.5 MB
Przeniesienie zmiennych ciągłych na koniec zbioru danych
print(cols)
mushroom = mushrooms[['class', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed',
'gill-attachment', 'gill-spacing', 'gill-color', 'stem-root',
'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type',
'spore-print-color', 'habitat', 'season', 'cap-diameter', 'stem-height', 'stem-width']]
mushrooms = pd.DataFrame(mushroom)
['class', 'cap-diameter', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color', 'stem-height', 'stem-width', 'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color', 'habitat', 'season']
mushrooms.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61069 entries, 0 to 61068 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 class 61069 non-null category 1 cap-shape 61069 non-null category 2 cap-surface 46949 non-null category 3 cap-color 61069 non-null category 4 does-bruise-or-bleed 61069 non-null category 5 gill-attachment 51185 non-null category 6 gill-spacing 36006 non-null category 7 gill-color 61069 non-null category 8 stem-root 9531 non-null category 9 stem-surface 22945 non-null category 10 stem-color 61069 non-null category 11 veil-type 3177 non-null category 12 veil-color 7413 non-null category 13 has-ring 61069 non-null category 14 ring-type 58598 non-null category 15 spore-print-color 6354 non-null category 16 habitat 61069 non-null category 17 season 61069 non-null category 18 cap-diameter 61069 non-null float64 19 stem-height 61069 non-null float64 20 stem-width 61069 non-null float64 dtypes: category(18), float64(3) memory usage: 2.5 MB
Liczebność grup w zmiennej celu:
mushrooms['class'].value_counts()
p 33888 e 27181 Name: class, dtype: int64
Podsumownie dla zmiennych ciągłych:
mushrooms.describe()
| cap-diameter | stem-height | stem-width | |
|---|---|---|---|
| count | 61069.000000 | 61069.000000 | 61069.000000 |
| mean | 6.733854 | 6.581538 | 12.149410 |
| std | 5.264845 | 3.370017 | 10.035955 |
| min | 0.380000 | 0.000000 | 0.000000 |
| 25% | 3.480000 | 4.640000 | 5.210000 |
| 50% | 5.860000 | 5.950000 | 10.190000 |
| 75% | 8.540000 | 7.740000 | 16.570000 |
| max | 62.340000 | 33.920000 | 103.910000 |
Wśród zmiennych ciągłych występują wartości zerowe, które w przypadku wymiarów nie mają sensu i na etapie czyszczenia danych zostanie to przeanalizowane. Pojawiają się również wartości znacznie wyższe w stosunku do Q3, co może świadczyć o punktach oddalonych, ale to rónież zostanie zweryfikowane w dalszych krokach.
Sprawdzenie liczebności NA w poszczególnych zmiennych
NA_numb = mushrooms.isnull().sum()
NA_perc = mushrooms.isnull().sum()*100/len(mushrooms)
df_NA = pd.DataFrame({'Number of NA' : NA_numb,
'% of data' : NA_perc})
print(df_NA.sort_values(by = '% of data', ascending=False))
Number of NA % of data veil-type 57892 94.797688 spore-print-color 54715 89.595376 veil-color 53656 87.861272 stem-root 51538 84.393064 stem-surface 38124 62.427746 gill-spacing 25063 41.040462 cap-surface 14120 23.121387 gill-attachment 9884 16.184971 ring-type 2471 4.046243 class 0 0.000000 stem-height 0 0.000000 cap-diameter 0 0.000000 season 0 0.000000 habitat 0 0.000000 stem-color 0 0.000000 has-ring 0 0.000000 cap-shape 0 0.000000 gill-color 0 0.000000 does-bruise-or-bleed 0 0.000000 cap-color 0 0.000000 stem-width 0 0.000000
NA's stanowią >60% danych. Zmienne zostaną dodatkowo przeanalizowane na etapie EDA i wówczas zostanie podjęta decyzja jak rozwiązać kwestię brakujących danych. NAZmiana jendostek zmiennej stem-width z [mm] na [cm] i rozbicie zmiennych na osobne zbiory obserwacji dla każdej kategorii zmiennej celu:
mushrooms['stem-width'] = mushrooms['stem-width']/10
mushrooms_p = mushrooms[mushrooms['class'] == 'p']
mushrooms_e = mushrooms[mushrooms['class'] == 'e']
col_con = ['cap-diameter', 'stem-width', 'stem-height']
Histogram dla każdej zmiennej ciągłej z podziałem na zmienną celu class, graficznie pokaże różnice między rozkładami oraz nienormalizując osi Y można wychwycić potencjalne punkty oddalone.
for col in col_con:
if np.mean(mushrooms[col]) < 2:
binwidth=0.2
else:
binwidth=1
plt.hist(mushrooms_e[col], alpha = 0.7, color = 'forestgreen', label = 'e', edgecolor='grey', bins=np.arange(min(mushrooms[col]), max(mushrooms[col]) + binwidth, binwidth))
plt.hist(mushrooms_p[col], alpha = 0.7, color = 'crimson', label='p', edgecolor='grey', bins=np.arange(min(mushrooms[col]), max(mushrooms[col]) + binwidth, binwidth))
plt.title("Histogram dla zmiennej " + str(col))
plt.ylabel('count')
plt.xlabel(col + ' [cm]')
plt.legend(loc='upper right')
plt.show()
plt.clf()
<Figure size 432x288 with 0 Axes>
Powyższe histogramy pokazują, że rozkłady dla trujących grzybów ('p') i jadalnych ('e') nie różnią się bardzo między sobą. W każdym przypadku rozkład jest prawostronnie skośny. Dodatkowo, na histogramach widać jak wiele wartości zerowych jest w zbiorze danych.
Sprawdzenie czy występują punkty oddalone wg założenia, że punkt x jest oddalony, gdy: $$(x < Q_1 - 1.5*IQR) \ \lor \ (x > Q_3 + 1.5*IQR)$$
def outliers(x, s, e):
# x = dataframe
# s = index of first column to take
# e = index of last column to take
p = x[:]
for i in range(s, e+1):
Q1 = np.quantile(p.iloc[:,i], q=0.25, axis=0)
Q3 = np.quantile(p.iloc[:,i], q=0.75, axis=0)
iqr = Q3 - Q1
low = Q1 - iqr*1.5
up = Q3 + iqr*1.5
p.iloc[:,i] = np.logical_or(p.iloc[:,i] < low, p.iloc[:,i] > up)
p['outliers_numb'] = np.sum(p.iloc[:,s:e+1], axis=1)
x['outliers_numb'] = p['outliers_numb']
tot = sum(x['outliers_numb'])
totr = sum(x['outliers_numb'] > 0)
perc = (tot*100)/(len(x)*(len(x.columns)))
percr = (totr*100)/len(x)
print("Total number of outliers: ", round(tot, 0))
print("% of outliers: ", round(perc, 2))
print("Total number of rows with outliers: ", round(totr, 0))
print("% of rows with outliers: ", round(percr, 2))
print("Rows with outliers: ")
print(x[x['outliers_numb'] > 0])
#example
w = {'col1' : [1, 2, 3, 4, 5, 90, 6],
'col2' : [13, 60, 13, 18, 13, 12, 0],
'col3' : [1, 899, 5, 4, 3, 8, 6]}
w = pd.DataFrame(w)
print(w)
outliers(w,0,2)
col1 col2 col3 0 1 13 1 1 2 60 899 2 3 13 5 3 4 18 4 4 5 13 3 5 90 12 8 6 6 0 6 Total number of outliers: 4 % of outliers: 14.29 Total number of rows with outliers: 3 % of rows with outliers: 42.86 Rows with outliers: col1 col2 col3 outliers_numb 1 2 60 899 2 5 90 12 8 1 6 6 0 6 1
outliers(mushrooms,s=18,e=20)
Total number of outliers: 7536
% of outliers: 0.56
Total number of rows with outliers: 5340
% of rows with outliers: 8.74
Rows with outliers:
class cap-shape cap-surface cap-color does-bruise-or-bleed \
0 p x g o f
1 p x g o f
2 p x g o f
3 p f h e f
4 p x h o f
... ... ... ... ... ...
59578 p o NaN w f
59591 p o NaN w f
59618 p o NaN w f
59619 p o NaN w f
59646 p o NaN w f
gill-attachment gill-spacing gill-color stem-root stem-surface ... \
0 e NaN w s y ...
1 e NaN w s y ...
2 e NaN w s y ...
3 e NaN w s y ...
4 e NaN w s y ...
... ... ... ... ... ... ...
59578 f f f NaN g ...
59591 f f f NaN g ...
59618 f f f NaN g ...
59619 f f f NaN g ...
59646 f f f NaN g ...
veil-color has-ring ring-type spore-print-color habitat season \
0 w t g NaN d w
1 w t g NaN d u
2 w t g NaN d w
3 w t p NaN d w
4 w t p NaN d w
... ... ... ... ... ... ...
59578 NaN t f NaN p a
59591 NaN t f NaN p u
59618 NaN t f NaN d a
59619 NaN t f NaN p a
59646 NaN t f NaN d a
cap-diameter stem-height stem-width outliers_numb
0 15.26 16.95 1.709 1
1 16.60 17.99 1.819 2
2 14.07 17.80 1.774 1
3 14.17 15.77 1.598 1
4 14.64 16.53 1.720 1
... ... ... ... ...
59578 5.33 6.45 3.551 1
59591 5.63 5.65 3.762 1
59618 4.89 6.20 3.507 1
59619 4.99 6.39 3.443 1
59646 4.57 5.45 3.466 1
[5340 rows x 22 columns]
Jak widać na podsumowniu, niecałe 9% obserwacji zawiera conajmniej 1 punkt oddalony.
Sprawdzenie czy punkty oddalone są skorelowane ze zmienną celu:
mushrooms_p = mushrooms[mushrooms['class'] == 'p']
mushrooms_e = mushrooms[mushrooms['class'] == 'e']
outl = {'outliers' : (1, 2, 3),
'p' : (len(mushrooms_p[mushrooms_p['outliers_numb'] == 1]),
len(mushrooms_p[mushrooms_p['outliers_numb'] == 2]),
len(mushrooms_p[mushrooms_p['outliers_numb'] == 3])),
'e' : (len(mushrooms_e[mushrooms_e['outliers_numb'] == 1]),
len(mushrooms_e[mushrooms_e['outliers_numb'] == 2]),
len(mushrooms_e[mushrooms_e['outliers_numb'] == 3]))
}
outl = pd.DataFrame(outl)
print(outl)
outliers p e 0 1 2005 1657 1 2 563 597 2 3 0 518
Korelacja jest widoczna jedynie w przypadku, gdy dla danej obserwacji występują 3 punkty oddalone, czyli w sytuacji, w której każda wartość zmiennej ciągłej jest outlierem, i takie obserwacje występują jedynie dla grzybów jadalnych ('e'), dlatego utworzona nowa zmienna 'outliers_numb' może się okazać pomocna w klasyfikacji, chciaż dotyczy on niewielkiej części wszystkich obserwacji.
Dla analizowanego zbioru danych, punkty oddalone nie będą usuwane lub zastępowane wartościami średnimi, ponieważ w tym przypadku punkty oddalone pojawiają się tylko jako 'duże' wartości, a krótki research udowodnił, że w przyrodzie występują okazy o podobnych wymiarach.
Sprawdzenie ile wymiarów równych 0 pojawia się dla każdej zmiennej:
zero_diam = { 'zmienna' : col_con,
'zero' : (len(mushrooms[mushrooms['cap-diameter'] == 0]), len(mushrooms[mushrooms['stem-height'] == 0]),
len(mushrooms[mushrooms['stem-width'] == 0]))
}
zero_diam = pd.DataFrame(zero_diam)
print(zero_diam)
zmienna zero 0 cap-diameter 0 1 stem-width 1059 2 stem-height 1059
Wynika z tego, że wartości równe 0 pojawiają się wyłącznie dla szerokości i wysokości trzonu, do tego jest ich tyle samo dla obu zmiennych. Ponownie, w wyniku researchu okazuje się, że w przyrodzie występują okazy pozbawione widocznego trzonu, więc wartości te nie są bezsensowne.
Histogram dla każdej zmiennej kategorycznej z podziałem na zmienną celu class:
col_cat = ['cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color',
'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color','habitat',
'season']
for col in col_cat:
plt.figure()
sns.countplot(x=col, hue='class', data=mushrooms, palette='Set2').set_title("Histogram dla zmiennej " + str(col))
Szczególną uwagę zwrócono na zmienne, w których są braki dane. Na wcześniejszych etapach analizy, okazało się, że jest to 9 zmiennych (kolejność od najbardziej wybrakowanych): veil-type, spore-print-color, veil-color, stem-root, stem-surface, gill-spacing, cap-surface, gill-attachment, ring-type.
veil-type : z wykresu wynika, że oprócz braku danych pojawia się tylko 1 kategoria 'u' = 'universal', dlatego zmienna nic nie wnosi do danych i zostanie usunięta;spore-print-color: pamietając, że klasy są dość zbalansowane, ale rekordów 'p' jest więcej niż 'e', to można założyć, że brakującyh danych dla tej zmiennej jest w podobnej ilości dla obu klas, dlatego zmienna niewiele wnosi i zostanie usunięta;veil-color: jak wyżej, klasy dla brakujących danych są zbalansowane, ale w tym przypadku 4 kategorie występują wyłącznie w przypadku klasy 'p' zmiennej celu, dlatego ta zmienna pozostanie;stem-root: jak wyżej;stem-surface: jak wyżej;gill-spacing: jak wyżej;cap-surface: jak wyżej;gill-attachment: jak wyżej;ring-type: jak wyżej;Wartości brakujące dla zmiennych, które zostały, będą zastąpione wartościami modalnymi.
mushrooms = mushrooms.drop(columns=['veil-type', 'spore-print-color'])
mushrooms.shape
(61069, 20)
temp = {'zmienna' : ('veil-color', 'stem-root', 'stem-surface', 'gill-spacing', 'cap-surface', 'gill-attachment', 'ring-type'),
'moda' : (mushrooms['veil-color'].dropna().mode(), mushrooms['stem-root'].dropna().mode(),
mushrooms['stem-surface'].dropna().mode(), mushrooms['gill-spacing'].dropna().mode(),
mushrooms['cap-surface'].dropna().mode(), mushrooms['gill-attachment'].dropna().mode(),
mushrooms['ring-type'].dropna().mode())}
temp = pd.DataFrame(temp)
print(temp)
zmienna moda 0 veil-color 0 w Name: veil-color, dtype: category Categ... 1 stem-root 0 b 1 s Name: stem-root, dtype: category... 2 stem-surface 0 s Name: stem-surface, dtype: category Cat... 3 gill-spacing 0 c Name: gill-spacing, dtype: category Cat... 4 cap-surface 0 t Name: cap-surface, dtype: category Cate... 5 gill-attachment 0 a Name: gill-attachment, dtype: category ... 6 ring-type 0 f Name: ring-type, dtype: category Catego...
mushrooms['veil-color'] = mushrooms['veil-color'].fillna('w')
mushrooms['stem-root'] = mushrooms['stem-root'].fillna('b')
mushrooms['stem-surface'] = mushrooms['stem-surface'].fillna('s')
mushrooms['gill-spacing'] = mushrooms['gill-spacing'].fillna('c')
mushrooms['cap-surface'] = mushrooms['cap-surface'].fillna('t')
mushrooms['gill-attachment'] = mushrooms['gill-attachment'].fillna('a')
mushrooms['ring-type'] = mushrooms['ring-type'].fillna('f')
mushrooms.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61069 entries, 0 to 61068 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 class 61069 non-null category 1 cap-shape 61069 non-null category 2 cap-surface 61069 non-null category 3 cap-color 61069 non-null category 4 does-bruise-or-bleed 61069 non-null category 5 gill-attachment 61069 non-null category 6 gill-spacing 61069 non-null category 7 gill-color 61069 non-null category 8 stem-root 61069 non-null category 9 stem-surface 61069 non-null category 10 stem-color 61069 non-null category 11 veil-color 61069 non-null category 12 has-ring 61069 non-null category 13 ring-type 61069 non-null category 14 habitat 61069 non-null category 15 season 61069 non-null category 16 cap-diameter 61069 non-null float64 17 stem-height 61069 non-null float64 18 stem-width 61069 non-null float64 19 outliers_numb 61069 non-null int64 dtypes: category(16), float64(3), int64(1) memory usage: 2.8 MB
Ze względu na dość dużą ilość kategorii w zmiennych jakościowych, zostanie zastosowany label encoding, ponieważ one-hot encoding wygenerowałby bardzo dużą liczbę predyktorów.
from sklearn import preprocessing
label_encoder = preprocessing.LabelEncoder()
mushrooms.iloc[:, 0:16].apply(label_encoder.fit_transform)
| class | cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 3 |
| 1 | 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 2 |
| 2 | 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 3 |
| 3 | 1 | 2 | 3 | 1 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 5 | 0 | 3 |
| 4 | 1 | 6 | 3 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 5 | 0 | 3 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 61064 | 1 | 5 | 7 | 11 | 0 | 3 | 2 | 2 | 0 | 5 | 12 | 4 | 0 | 1 | 0 | 0 |
| 61065 | 1 | 2 | 7 | 11 | 0 | 3 | 2 | 2 | 0 | 5 | 12 | 4 | 0 | 1 | 0 | 0 |
| 61066 | 1 | 5 | 7 | 11 | 0 | 3 | 2 | 2 | 0 | 5 | 12 | 4 | 0 | 1 | 0 | 2 |
| 61067 | 1 | 2 | 7 | 11 | 0 | 3 | 2 | 2 | 0 | 5 | 12 | 4 | 0 | 1 | 0 | 2 |
| 61068 | 1 | 5 | 7 | 11 | 0 | 3 | 2 | 2 | 0 | 5 | 12 | 4 | 0 | 1 | 0 | 2 |
61069 rows × 16 columns
mushrooms.head()
| class | cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | cap-diameter | stem-height | stem-width | outliers_numb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | p | x | g | o | f | e | c | w | s | y | w | w | t | g | d | w | 15.26 | 16.95 | 1.709 | 1 |
| 1 | p | x | g | o | f | e | c | w | s | y | w | w | t | g | d | u | 16.60 | 17.99 | 1.819 | 2 |
| 2 | p | x | g | o | f | e | c | w | s | y | w | w | t | g | d | w | 14.07 | 17.80 | 1.774 | 1 |
| 3 | p | f | h | e | f | e | c | w | s | y | w | w | t | p | d | w | 14.17 | 15.77 | 1.598 | 1 |
| 4 | p | x | h | o | f | e | c | w | s | y | w | w | t | p | d | w | 14.64 | 16.53 | 1.720 | 1 |
mushrooms.iloc[:, 0:16] = mushrooms.iloc[:, 0:16].apply(label_encoder.fit_transform)
mushrooms.head()
| class | cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | cap-diameter | stem-height | stem-width | outliers_numb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 3 | 15.26 | 16.95 | 1.709 | 1 |
| 1 | 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 2 | 16.60 | 17.99 | 1.819 | 2 |
| 2 | 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 3 | 14.07 | 17.80 | 1.774 | 1 |
| 3 | 1 | 2 | 3 | 1 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 5 | 0 | 3 | 14.17 | 15.77 | 1.598 | 1 |
| 4 | 1 | 6 | 3 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 5 | 0 | 3 | 14.64 | 16.53 | 1.720 | 1 |
cor = mushrooms.corr("pearson")
plt.figure(figsize=(14,12))
sns.heatmap(cor, annot=True, cmap=plt.cm.Reds)
plt.show()
Widoczna jest korelacja pomiędzy zmiennymi cap-diameter i stem-width, co jest uzasadnione w przypadku wymiarów grzyba, ale predyktory narazie zostaną w zbiorze.
y = mushrooms['class'].values
print(y)
[1 1 1 ... 1 1 1]
x = mushrooms.drop(['class'], axis=1)
x.head()
| cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | cap-diameter | stem-height | stem-width | outliers_numb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 3 | 15.26 | 16.95 | 1.709 | 1 |
| 1 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 2 | 16.60 | 17.99 | 1.819 | 2 |
| 2 | 6 | 2 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 2 | 0 | 3 | 14.07 | 17.80 | 1.774 | 1 |
| 3 | 2 | 3 | 1 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 5 | 0 | 3 | 14.17 | 15.77 | 1.598 | 1 |
| 4 | 6 | 3 | 6 | 0 | 2 | 0 | 10 | 4 | 7 | 11 | 4 | 1 | 5 | 0 | 3 | 14.64 | 16.53 | 1.720 | 1 |
from sklearn.model_selection import train_test_split
idx_train, idx_test = train_test_split(np.arange(x.shape[0]),
test_size=0.1,
random_state=123)
x_train, x_test = x.iloc[idx_train, :], x.iloc[idx_test, :]
y_train, y_test = y[idx_train], y[idx_test]
x_train.shape, x_test.shape, y_train.shape, y_test.shape
((54962, 19), (6107, 19), (54962,), (6107,))
x_train.head()
| cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | cap-diameter | stem-height | stem-width | outliers_numb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 43602 | 6 | 4 | 5 | 1 | 2 | 0 | 7 | 0 | 7 | 11 | 4 | 1 | 5 | 3 | 0 | 10.52 | 8.89 | 1.696 | 0 |
| 42350 | 4 | 7 | 10 | 0 | 2 | 0 | 4 | 0 | 5 | 11 | 4 | 1 | 0 | 4 | 0 | 4.83 | 4.50 | 1.142 | 0 |
| 17288 | 2 | 8 | 11 | 0 | 5 | 1 | 11 | 0 | 3 | 12 | 4 | 0 | 1 | 0 | 0 | 6.09 | 5.54 | 1.440 | 0 |
| 45320 | 0 | 10 | 5 | 0 | 0 | 0 | 4 | 0 | 5 | 11 | 4 | 0 | 1 | 0 | 3 | 3.05 | 6.27 | 0.303 | 0 |
| 3964 | 6 | 10 | 11 | 0 | 0 | 0 | 10 | 0 | 7 | 6 | 4 | 1 | 6 | 1 | 0 | 3.13 | 6.19 | 0.355 | 0 |
y_train
array([0, 0, 1, ..., 1, 1, 0])
W ramach pracy zostaną porównane 3 modele opierające się na uczeniu nadzorowanym:
model 1: klasyczna regresja logistyczna bez regularyzacji
model 2: maszyna wektorów nośnych (SVM)
model 3: las losowy (random forest).
Regresja logistyczna jest jednym z najbardziej optymalnych algorytmów do klasyfikacji binarnej, dlatego stanowi pierwszy wybór. Jest to szczególny przypadek regresji liniowej, w którym funkcję wiążącą jest funkcja logitowa, która przekształca prawdopodobieństwo na logarytm ilorazu szans.
U podstaw metody wektorów nośnych (Support Vector Machines - SVM) leży koncepcja szukania hiperpłaszczyzny, która 'najlepiej' rozdziela klasy zbioru uczącego. Maszyna wektorów nośnych szuka hiperpłaszczyzny maksymalizującej odległość do najbliższego punktu w każdej z klas. Gdy płaszczyzna oddzielająca klasy nie istnieje, dane mogą zostać zmapowane do przestrzeni o większej liczbie wymiarów, co pozwala na separację klas.
Technika lasów losowych polega na otrzymaniu 'mocnego modelu' (ensemblera) poprzez tworzenie wielu drzew decyzyjnych (które są tzw. 'słabymi modelami') i łączenie ich wyników. W przypadku klasyfikacji, wynik jest określany na drodze losowania.
Modele zostaną porównane na podstawie metryk:
Accuracy, ponieważ jest bardzo intuicyjna, mówi o stosunku poprawnie sklasyfikowanych obserwacji do ilości wszystkich klasyfikacji oraz w projekcie założono, że klasy nie są niezbalansowane
F1, która jest średnią harmoniczną z wartości recall oraz precision i uwzględnia je równomiernie.
gdzie:
$$ TP - klasa \ prawdziwie \ pozytywna$$$$ TN - klasa \ prawdziwie \ negatywna$$$$ FP - klasa \ fałszywie \ pozytywna$$$$ FN - klasa \ fałszywie \ negatywna$$W pierwszej części, metryki dla modeli zostaną wyznaczone poprzez losowy podział zbioru danych na train=90%, test=10%. W drugiej części, zostaną wyznaczone średnie wartości metryk dla tych samych modeli w wyniku walidacji krzyżowej (k-folds cross-valiadtion), gdzie k=10.
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
scores = {'model':[],
'accuracy_train' :[],
'accuracy_test' : [],
'F1_train' : [],
'F1_test' : []}
from sklearn.linear_model import LogisticRegression
model_1_lr = LogisticRegression(solver='lbfgs', max_iter=400)
model_1_lr.fit(x_train, y_train)
y_pred_train = model_1_lr.predict(x_train)
y_pred_test = model_1_lr.predict(x_test)
acc_train = accuracy_score(y_train, y_pred_train)
acc_test = accuracy_score(y_test, y_pred_test)
f1_train = f1_score(y_train, y_pred_train)
f1_test = f1_score(y_test, y_pred_test)
scores['model'].append('model_1_lr')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr'],
'accuracy_train': [0.6614024234925949],
'accuracy_test': [0.6685770427378418],
'F1_train': [0.7108901662265029],
'F1_test': [0.7195121951219513]}
from sklearn.svm import SVC
model_2_svm = SVC()
model_2_svm.fit(x_train, y_train)
y_pred_train = model_2_svm.predict(x_train)
y_pred_test = model_2_svm.predict(x_test)
acc_train = accuracy_score(y_train, y_pred_train)
acc_test = accuracy_score(y_test, y_pred_test)
f1_train = f1_score(y_train, y_pred_train)
f1_test = f1_score(y_test, y_pred_test)
scores['model'].append('model_2_svm')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr', 'model_2_svm'],
'accuracy_train': [0.6614024234925949, 0.9256395327680943],
'accuracy_test': [0.6685770427378418, 0.9177992467660062],
'F1_train': [0.7108901662265029, 0.9318140109110928],
'F1_test': [0.7195121951219513, 0.9253864447086803]}
Czas uczenia powyższego modelu jest znacznie dłuższy niż w przypadku regresji logistycznej jak i lasu losowego. Wynika to z tego, że wymagania obliczeniowe i pamięciowe rosną wraz z liczbą wektorów treningowych (współrzędnych indywidualnej obserwacji). Domyślnie, SVC z pakietu scikit-learn używa nieliniowej funkcji kernela 'rbf', która dodtkowo zwiększa złożoność obliczeniową. Hiperparametr C modelu (współczynnik regularyzacji modelu) jest domyślnie ustawiony jako 1. Zminiejszając go, siła penalizacji za błędną klasyfikację maleje.
Dla porównania, poniżej model liniowego klasyfikatora SVM ze mniejszoną wartością hiperparametru C, który powinien lepiej się skalować do dużych próbek:
from sklearn.svm import LinearSVC
model_2l_svm = LinearSVC(C = 0.01)
model_2l_svm.fit(x_train, y_train)
y_pred_train = model_2l_svm.predict(x_train)
y_pred_test = model_2l_svm.predict(x_test)
acc_train = accuracy_score(y_train, y_pred_train)
acc_test = accuracy_score(y_test, y_pred_test)
f1_train = f1_score(y_train, y_pred_train)
f1_test = f1_score(y_test, y_pred_test)
scores['model'].append('model_2l_svm')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr', 'model_2_svm', 'model_2l_svm'],
'accuracy_train': [0.6614024234925949,
0.9256395327680943,
0.6605654816054729],
'accuracy_test': [0.6685770427378418, 0.9177992467660062, 0.6684132962174554],
'F1_train': [0.7108901662265029, 0.9318140109110928, 0.7095528708431934],
'F1_test': [0.7195121951219513, 0.9253864447086803, 0.7187109320738992]}
from sklearn.ensemble import RandomForestClassifier
model_3_rf = RandomForestClassifier(n_estimators=40)
model_3_rf.fit(x_train, y_train)
y_pred_train = model_3_rf.predict(x_train)
y_pred_test = model_3_rf.predict(x_test)
acc_train = accuracy_score(y_train, y_pred_train)
acc_test = accuracy_score(y_test, y_pred_test)
f1_train = f1_score(y_train, y_pred_train)
f1_test = f1_score(y_test, y_pred_test)
scores['model'].append('model_3_rf')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr', 'model_2_svm', 'model_2l_svm', 'model_3_rf'],
'accuracy_train': [0.6614024234925949,
0.9256395327680943,
0.6605654816054729,
1.0],
'accuracy_test': [0.6685770427378418,
0.9177992467660062,
0.6684132962174554,
1.0],
'F1_train': [0.7108901662265029, 0.9318140109110928, 0.7095528708431934, 1.0],
'F1_test': [0.7195121951219513, 0.9253864447086803, 0.7187109320738992, 1.0]}
from sklearn.model_selection import KFold
k = 10
kf = KFold(n_splits=10, shuffle = True, random_state = 123)
acc_train_scores = []
acc_test_scores = []
f1_train_scores = []
f1_test_scores = []
for train_index, test_index in kf.split(x):
x_train, x_test, y_train, y_test = x.iloc[train_index], x.iloc[test_index], y[train_index], y[test_index]
model_4_lr_cv = LogisticRegression(solver='lbfgs', max_iter=400)
model_4_lr_cv.fit(x_train, y_train)
y_pred_train = model_4_lr_cv.predict(x_train)
y_pred_test = model_4_lr_cv.predict(x_test)
acc_train_scores.append(accuracy_score(y_train, y_pred_train))
acc_test_scores.append(accuracy_score(y_test, y_pred_test))
f1_train_scores.append(f1_score(y_train, y_pred_train))
f1_test_scores.append(f1_score(y_test, y_pred_test))
acc_train = sum(acc_train_scores)/k
acc_test = sum(acc_test_scores)/k
f1_train = sum(f1_train_scores)/k
f1_test = sum(f1_test_scores)/k
scores['model'].append('model_4_lr_cv')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr',
'model_2_svm',
'model_2l_svm',
'model_3_rf',
'model_4_lr_cv'],
'accuracy_train': [0.6614024234925949,
0.9256395327680943,
0.6605654816054729,
1.0,
0.6621271717463306],
'accuracy_test': [0.6685770427378418,
0.9177992467660062,
0.6684132962174554,
1.0,
0.6616939928840793],
'F1_train': [0.7108901662265029,
0.9318140109110928,
0.7095528708431934,
1.0,
0.7117911265294985],
'F1_test': [0.7195121951219513,
0.9253864447086803,
0.7187109320738992,
1.0,
0.7115919192364574]}
acc_train_scores = []
acc_test_scores = []
f1_train_scores = []
f1_test_scores = []
# SVM z kernelem linearnym, ponieważ uczenie w walidacji krzyżowej modelu SVM z kernelem 'rbf' i C=1, trwało >3h
for train_index, test_index in kf.split(x):
x_train, x_test, y_train, y_test = x.iloc[train_index], x.iloc[test_index], y[train_index], y[test_index]
model_5_svm_l_cv = LinearSVC(C = 0.01)
model_5_svm_l_cv.fit(x_train, y_train)
y_pred_train = model_5_svm_l_cv.predict(x_train)
y_pred_test = model_5_svm_l_cv.predict(x_test)
acc_train_scores.append(accuracy_score(y_train, y_pred_train))
acc_test_scores.append(accuracy_score(y_test, y_pred_test))
f1_train_scores.append(f1_score(y_train, y_pred_train))
f1_test_scores.append(f1_score(y_test, y_pred_test))
acc_train = sum(acc_train_scores)/k
acc_test = sum(acc_test_scores)/k
f1_train = sum(f1_train_scores)/k
f1_test = sum(f1_test_scores)/k
scores['model'].append('model_5_svm_l_cv')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr',
'model_2_svm',
'model_2l_svm',
'model_3_rf',
'model_4_lr_cv',
'model_5_svm_l_cv'],
'accuracy_train': [0.6614024234925949,
0.9256395327680943,
0.6605654816054729,
1.0,
0.6621271717463306,
0.6615613273454789],
'accuracy_test': [0.6685770427378418,
0.9177992467660062,
0.6684132962174554,
1.0,
0.6616939928840793,
0.6607933360690569],
'F1_train': [0.7108901662265029,
0.9318140109110928,
0.7095528708431934,
1.0,
0.7117911265294985,
0.7105024387361407],
'F1_test': [0.7195121951219513,
0.9253864447086803,
0.7187109320738992,
1.0,
0.7115919192364574,
0.7100050437936188]}
acc_train_scores = []
acc_test_scores = []
f1_train_scores = []
f1_test_scores = []
for train_index, test_index in kf.split(x):
x_train, x_test, y_train, y_test = x.iloc[train_index], x.iloc[test_index], y[train_index], y[test_index]
model_6_rf_cv = RandomForestClassifier(n_estimators=40)
model_6_rf_cv.fit(x_train, y_train)
y_pred_train = model_6_rf_cv.predict(x_train)
y_pred_test = model_6_rf_cv.predict(x_test)
acc_train_scores.append(accuracy_score(y_train, y_pred_train))
acc_test_scores.append(accuracy_score(y_test, y_pred_test))
f1_train_scores.append(f1_score(y_train, y_pred_train))
f1_test_scores.append(f1_score(y_test, y_pred_test))
acc_train = sum(acc_train_scores)/k
acc_test = sum(acc_test_scores)/k
f1_train = sum(f1_train_scores)/k
f1_test = sum(f1_test_scores)/k
scores['model'].append('model_6_rf_cv')
scores['accuracy_train'].append(acc_train)
scores['accuracy_test'].append(acc_test)
scores['F1_train'].append(f1_train)
scores['F1_test'].append(f1_test)
scores
{'model': ['model_1_lr',
'model_2_svm',
'model_2l_svm',
'model_3_rf',
'model_4_lr_cv',
'model_5_svm_l_cv',
'model_6_rf_cv'],
'accuracy_train': [0.6614024234925949,
0.9256395327680943,
0.6605654816054729,
1.0,
0.6621271717463306,
0.6615613273454789,
1.0],
'accuracy_test': [0.6685770427378418,
0.9177992467660062,
0.6684132962174554,
1.0,
0.6616939928840793,
0.6607933360690569,
0.9999345013918454],
'F1_train': [0.7108901662265029,
0.9318140109110928,
0.7095528708431934,
1.0,
0.7117911265294985,
0.7105024387361407,
1.0],
'F1_test': [0.7195121951219513,
0.9253864447086803,
0.7187109320738992,
1.0,
0.7115919192364574,
0.7100050437936188,
0.9999406330536356]}
scores = pd.DataFrame(scores)
print(scores)
model accuracy_train accuracy_test F1_train F1_test 0 model_1_lr 0.661402 0.668577 0.710890 0.719512 1 model_2_svm 0.925640 0.917799 0.931814 0.925386 2 model_2l_svm 0.660565 0.668413 0.709553 0.718711 3 model_3_rf 1.000000 1.000000 1.000000 1.000000 4 model_4_lr_cv 0.662127 0.661694 0.711791 0.711592 5 model_5_svm_l_cv 0.661561 0.660793 0.710502 0.710005 6 model_6_rf_cv 1.000000 0.999935 1.000000 0.999941
Interpretacja i porównanie 3 modeli z wykorzystaniem pakietu DALEX.
Porównanie wpływu poszczególnych predyktorów w modelach (pierwszy histogram) oraz porównanie klasyfikacji losowej obserwacji ze zbioru dla każdego modelu (drugi histogram):
import dalex as dx
new_observation = x.iloc[6075:6076, :]
new_observation.head()
| cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | cap-diameter | stem-height | stem-width | outliers_numb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 6075 | 6 | 8 | 2 | 0 | 1 | 0 | 10 | 0 | 5 | 6 | 4 | 0 | 1 | 0 | 0 | 5.79 | 5.54 | 1.072 | 0 |
mushrooms.iloc[6075:6076, :]
| class | cap-shape | cap-surface | cap-color | does-bruise-or-bleed | gill-attachment | gill-spacing | gill-color | stem-root | stem-surface | stem-color | veil-color | has-ring | ring-type | habitat | season | cap-diameter | stem-height | stem-width | outliers_numb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 6075 | 1 | 6 | 8 | 2 | 0 | 1 | 0 | 10 | 0 | 5 | 6 | 4 | 0 | 1 | 0 | 0 | 5.79 | 5.54 | 1.072 | 0 |
def comp(model):
exp = dx.Explainer(model, x_train, y_train)
explanation = exp.model_parts()
explanation.plot()
plt.show()
exp.predict_parts(new_observation).plot()
comp(model_4_lr_cv)
Preparation of a new explainer is initiated -> data : 54963 rows 19 cols -> target variable : 54963 values -> model_class : sklearn.linear_model._logistic.LogisticRegression (default) -> label : Not specified, model's class short name will be used. (default) -> predict function : <function yhat_proba_default at 0x0000016836C938B0> will be used (default) -> predict function : Accepts pandas.DataFrame and numpy.ndarray. -> predicted values : min = 0.00389, mean = 0.555, max = 1.0 -> model type : classification will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -0.873, mean = -7.54e-06, max = 0.932 -> model_info : package sklearn A new explainer has been created!
Dla modelu opartego na regresji logistycznej, 5 najbardziej wpływowych predyktorów na klasyfikację stanowią zmienne (kolejno):
cap-diameter, stem-height, stem-surface, gill-spacing,veil-color.Drugi histogram przedstawia w którym miejscu znajduje się cutoff dla klasyfikacji (0.555) oraz jak została podjęta klasyfikacja dla konkretnej zmiennej. Sumarycznie, obserwacja została zaklasyfikowana po 'lewej stronie' separacji, czyli przyjmuje wartość 0, która oznacza 'e' - grzyb jadalny, co jesyt błędną klasyfikacją.
# SVM z kernelem linearnym, ponieważ znowu interpretacja modelu SVM z kernelem 'rbf' i C=1, trwała >2h
comp(model_5_svm_l_cv)
Preparation of a new explainer is initiated -> data : 54963 rows 19 cols -> target variable : 54963 values -> model_class : sklearn.svm._classes.LinearSVC (default) -> label : Not specified, model's class short name will be used. (default) -> predict function : <function yhat_default at 0x0000016836C93790> will be used (default) -> predict function : Accepts pandas.DataFrame and numpy.ndarray. -> predicted values : min = 0.0, mean = 0.612, max = 1.0 -> model type : classification will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -1.0, mean = -0.0579, max = 1.0 -> model_info : package sklearn A new explainer has been created!
Dla modelu opartego na SVM, 5 najbardziej wpływowych predyktorów na klasyfikację stanowią zmienne (kolejno):
cap-diameter, stem-height,gill-spacing, stem-surface,cap-shape.Całkiem podobnie, jak w przypadku regresji logistycznej.
Na drugim histogramie widać jak została podjęta klasyfikacja przykładowej obserwacji jako 1, czyli 'p'.
comp(model_6_rf_cv)
Preparation of a new explainer is initiated -> data : 54963 rows 19 cols -> target variable : 54963 values -> model_class : sklearn.ensemble._forest.RandomForestClassifier (default) -> label : Not specified, model's class short name will be used. (default) -> predict function : <function yhat_proba_default at 0x0000016836C938B0> will be used (default) -> predict function : Accepts pandas.DataFrame and numpy.ndarray. -> predicted values : min = 0.0, mean = 0.555, max = 1.0 -> model type : classification will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -0.325, mean = -6.1e-05, max = 0.3 -> model_info : package sklearn A new explainer has been created!
Dla modelu opartego na random forest, 5 najbardziej wpływowych predyktorów na klasyfikację stanowią zmienne (kolejno):
stem-width, gill-attachment, gill-spacing, stem-color,stem-surface.Zmienne istotne dla modelu lasu losowego różnią się w odniesieniu do wcześniejszych modeli.
Na drugim histogramie widać, że przykładowa obserwacja została zaklasyfikowana jako 1, czyli 'p'.
Czas uczenia zbudowanych modeli był stosunkowo długi (a zwłaszcza walidacja krzyżowa), co możnaby usprawnić poprzez redukcję liczby zmiennych na podstawie ich istotności (p-value). Szczególnie w przypadku SVM, czas uczenia, ze względu na duży wymiar zbioru danych, był bardzo nieefektywny.
W przypadku każdego modelu wartości metryk dla zbioru testowego i walidacyjnego nie różniły się znacząco, co może oznaczać, że model nie jest przeuczony. Dodatkowo, wartości metryk zostały zweryfikowane w walidacji krzyżowej 10-foldowej.
Model regresji logistycznej okazał się mieć najniższe metryki. Może to wynikać z współliniowości pomiędzy zmiennymi, z którą regresja logistyczna (jak i liniowa) sobie nie radzi. Na podstawie współczynnika VIF możnaby zweryfikować istniejącą współliniowość i usunąć zmienne o współczynniku VIF > 5. Dodatkowo, interpretacja modelu z wykorzystaniem pakietu DALEX, pokazała, że dla tego modelu wymiary, czyli zmienne ciągłe są predyktorami o największej istotności, dlatego ich współliniowość może znacząco obniżać efektywność klasyfikatora.
Model SVM z kernelem nieliniowym, osiągnął znacznie wyższe wartości metryk, natomiast czas uczenia był znacząco dłuższy. Model okazał się nieefektywny. Dla modelu linearnego SVM, z obniżoną wartością siły regularyzacji (C), uzyskano znacznie korzystniejszy czas uczenia, natomiast kosztem metryk, bo spadły do wartości porównywalnych z modelem regresji logistycznej.
Model lasu losowego okazał się być bardzo szybki pod względem czasu uczenia oraz uzyskał najwyższe wartości metryk (ACC i F1 zbliżone do 1), co jest dla mnie w pewnym stopniu alarmujące, ale jednocześnie nie jestem w stanie znaleźć wady modelu (wykonano walidację krzyżową oraz sam algorytm lasu losowego jest z założenia mniej podatny na przeuczenie), dlatego wybieram go jako najbardziej optymalny.
Realizacja projektu pozwoliła mi znaleźć moje 'słabe' strony w pracy zarówno z samym językiem Python jak i Jupyter Notebook. Gdybym realizowała go ponownie, postarałabym się bardziej zoptymalizować swoją pracę poprzez wykonanie części działań w R, a części w Pythonie - m.in. wykonałabym przynajmniej Eksploracyjną Analizę Danych w R, ponieważ pakiet ggplot2 jest mi bardziej znany i w moim subiektywnym odczuciu jest bardziej intuicyjny.